'''人类也能简单使用的kfol计算器,第2版,修改了武器防具的设定,变成列表.
  使用时必须双击.py文件,否则读取的in_file位置会出错.
  用Notepad++启动时,配置文件要放在D:\\software\\Notepad++下
  由于shutil.copy复制的文件也不同,现在设定为直接用Notepad++启动,
  修改的kf_setycyas.in文件也在D:\\software\\Notepad++下
  配置的前4个10是: 强壮、坚强、快速、睿智NPC的出现率,需要特殊研究就改这个
'''

### 在这里设定基本数据

### 计算器设置开始
## 计算器目录与exe文件,如果是32位请设定为'kfol2.exe'
#KFOL_PATH = r'E:\PC\kfol2_2.9.0.1'
KFOL_PATH = r'E:\PC\旧kfol2.9.2.2'
KFOL_EXE = 'kfol2_64.exe'
### 计算器设置结束

### 账户资料设置开始
## 'in_file'是kfol计算器使用的.in配置文件,单个账户的可直接设置为kfol.in
## 'total_point'是加点总数值,'base_hp'是体质加点为0时的hp值,打开争夺页面
## 用显示的最大hp减去20*体质加点计算,
## 'weapons'与'armors'是自己记录的配置文件的武器与防具,自定义一个名称,
## 然后指定[在配置文件中的编号, id].
## 看我的两个账户的设定:setycyas与工藤駆
SETYCYAS = {
  'in_file': 'kfol_setycyas.in',
  'total_point': 935,
  'base_hp': (679+35)*20,
  # 火弓属性虽然不强,但还是弓的威力大,实用
  'weapons':[
    ["火剑0", "65375363"], ["火弓0", "61438832"]
  ],
  # 冰反伤甲0是灵活甲,效果不好但有神秘;
  # 意体反伤甲0是体质,意志甲,没有神秘但效果好
  'armors':[
    ["冰反伤甲0", "61306325"], ["意体反伤甲0", "65372867"]
  ]
}

SANDCASTLE = {
  'in_file': 'kfol_sandcastle.in',
  'total_point': 455,
  'base_hp': 20*175, # 20*基础体质
  # 暴击弓最实用,平衡不如集中,即使集中的属性稍差
  'weapons':[
    ["暴击弓0", "65182438"], ["风弓0", "65278708"], ["平衡剑0", "65298424"]
  ],
  # 似乎反伤才是王道,仇恨也比不上
  'armors':[
    ["仇甲0", "56289820"], ["反伤甲0", "64763373"], ["反伤甲1", "65115325"],
    ["反伤甲2", "65182435"]
  ]
}
### 账户资料设置结束

import subprocess
import re
import os
import shutil
import sys

class Kfol:

  ## 指定计算器路径,exe文件,账户,
  ## 使用武器,护甲,计算步长,计算层,开始计算的点进行初始化
  def __init__(self, kfol_path, kfol_exe, 
    account, weapon_name, armor_name, step, floor, start_point):
    ## 保存参数
    self.kfol_path = kfol_path
    self.kfol_exe = kfol_exe
    self.account = account # 账户
    self.weapon_name = weapon_name
    self.weapon = -1 # 武器id
    for i in range(len(self.account['weapons'])):
      if (self.account['weapons'][i][0] == self.weapon_name):
        self.weapon = i
        break
    self.armor_name = armor_name
    self.armor = -1 # 护甲id
    for i in range(len(self.account['armors'])):
      if (self.account['armors'][i][0] == self.armor_name):
        self.armor = i
        break
    self.step = step # 搜索步长
    self.floor = floor # 计算层数
    ## 计算过程使用的变量
    self.cur_point = start_point # 当前最佳点
    ## 自定义的最大寻找次数
    self.MAX_SEARCH = 10
    ## 复制.in文件
    if account['in_file'] != 'kfol.in':
      #shutil.copyfile(os.path.join(sys.path[0], account['in_file']),
      #  os.path.join(sys.path[0], 'kfol.in'))
      shutil.copyfile(account['in_file'],'kfol.in')
      print('复制配置文件成功!')
    else:
      print('目前设定不需要复制配置文件!')
    return
  
  ## 获取一个点的胜率,返回一个%浮点数,若计算失败返回-100.0
  def getPointWinrate(self, point):
    ## Message to send to kfol2
    hp = self.account['base_hp']+20*point[1]
    weapon = self.weapon
    armor = self.armor
    msg = 'b {floor} {hp} {point[0]} {point[1]} {point[2]} {point[3]} '
    msg += '{point[4]} {point[5]} {weapon} {armor}\r\n'
    msg = msg.format(
      floor = self.floor, hp = hp, point = point, 
      weapon = weapon, armor = armor
    )
    print("Testing:")
    print(msg[:-2])
    ## 打开计算器
    proc = subprocess.Popen([os.path.join(self.kfol_path, 
      self.kfol_exe)], 
      stdin = subprocess.PIPE, 
      stdout = subprocess.PIPE
    )
    ## Send message(bytes) to kfol2, and get winrate
    respBytes = proc.communicate(input = msg.encode())[0]
    resp = respBytes.decode().upper()
    #print(resp)
    #input('')
    #assert(1<0)
    matches = re.findall(r'WIN RATE SUMMARY = (\d+\.\d+)', resp)
    ## Return
    result = -100.0
    if matches:
      result = float(matches[-1])
    print('result is:', str(result)+'%\n')
    return result
  
  ## 获取一个点的附近的点集,返回一个列表
  def getNearPoints(self, point):
    result = []
    ## 如果当前最佳点的分配已经满了,则获取所有的两项分配的一增一减点集合
    if (sum(point) == self.account['total_point']):
      ## i是增加项,j是减少项
      for i in range(6):
        for j in range(6):
          ## 如果增加项就是减少项,或减少项已经是1不能再减少,跳过
          if (i == j) or (point[j] == 1):
            continue
          # 计算步长为指定步长,或可以变动的最大数值
          step = min(self.step, point[j]-1)
          newPoint = point.copy()
          newPoint[i] += step
          newPoint[j] -= step
          result.append(newPoint)
    ## 如果当前最佳点的分配未满,则获取所有单项增加的点
    else:
      step = min(self.step, self.account['total_point']-sum(self.cur_point))
      print(step)
      for i in range(6):
        newPoint = point.copy()
        newPoint[i] += step
        result.append(newPoint)
    ## 返回
    return result
  
  ## 优化一个点,在指定点附近的点集找出最大优化,返回最大优化的点与其胜率,
  ## 返回的也可能是给定点自己,这时表示已经不能进行附近优化
  ## winrate是指定的点的胜率,如果传入可以少计算一次,
  ## 如果传入负数则表示还没有计算,需要重新算一次
  def optimizePoint(self, point, winrate = -100.0):
    ## 指定当前点与胜率
    cur_point = point
    if winrate < 0:
      cur_winrate = self.getPointWinrate(cur_point)
    else:
      cur_winrate = winrate
    ## 获取附近点集,然后计算胜率找出最大值
    nearPoints = self.getNearPoints(point)
    for near_point in nearPoints:
      near_winrate = self.getPointWinrate(near_point)
      if near_winrate > cur_winrate:
        cur_point = near_point
        cur_winrate = near_winrate
    ## 返回
    return (cur_point, cur_winrate)
    
  ## 连续寻找最大优化,不断找附近的最大优化点,
  ## 直到附近不再有优化,或次数超过指定数值.返回优化点,胜率
  def getMaxOptimizePoint(self, point):
    cur_point = point
    cur_winrate = -100.0
    for i in range(self.MAX_SEARCH):
      new_point, new_winrate = self.optimizePoint(cur_point, cur_winrate)
      print('第 {0} 次优化完成,最佳点: {1} 胜率: {2}\n'.format(
        i+1, new_point, new_winrate
      ))
      if cur_point == new_point:
        cur_point, cur_winrate = new_point, new_winrate
        break
      else:
        cur_point, cur_winrate = new_point, new_winrate
    return cur_point, cur_winrate
    
  ## Main
  def main(self):
    print('开始进行优化,配置文件是:', self.account['in_file'])
    print('计算目标是:{weapon}, {armor}, {floor}层, 步长{step}'.format(
      weapon = self.weapon_name, armor = self.armor_name, floor = self.floor, 
      step = self.step))
    x = input('确认后输入回车开始,任意键加回车退出')
    if(len(x) == 0):
      self.getMaxOptimizePoint(self.cur_point)
    return
    

if __name__ == '__main__':
  ### 测试设置开始

  account = SETYCYAS 
  weapon_name = "火弓0"
  armor_name = "冰反伤甲0"
  armor_name = "意体反伤甲0"
  step = 50
  floor = 199
  
  start_point = [1, 160, 1, 655, 1, 117] 
  # 火弓0意体反伤甲0,201层80胜率,200层以内胜率99.99;
  # 200层内火弓0冰反伤甲0也可用胜率99.99
  start_point = [1, 235, 1, 330, 1, 367]
  # 火弓0冰反伤甲0,201层胜率78.3%
  start_point = [1, 35, 1, 580, 1, 317]
  # 火弓0意体反伤甲0,199,200层高胜率,目的是在增加hp上限的情况下找胜算
  start_point = [1, 539, 101, 1, 1, 292]
  
  '''
  account = SANDCASTLE
  weapon_name = "暴击弓0"
  #armor_name = "仇甲0"
  armor_name = "反伤甲0"
  step = 15
  floor = 80
  
  #暴击弓0反伤甲0,90层65,89层92,反伤流才是正道?!
  start_point = [11, 127, 189, 126, 1, 1]
  '''
  
  ### 测试设置结束
  
  kf = Kfol(KFOL_PATH, KFOL_EXE, account, weapon_name, armor_name, 
    step, floor, start_point)
  try:
    kf.main()
    input('优化完成,输入回车结束')
  except Exception as e:
    print('出现错误', e)
    input('输入回车结束')
  